package de.islanders.shared;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

@SuppressWarnings({"unchecked", "rawtypes"})
/**
 * Similar to Math.Random, but used for biased randomizations
 */
public class RandomBias {
	
	private boolean liveDistrib;
	private long scale;
	private LinkedList<RandomObject> objects;
	private boolean updated;
	
	/**
	 * This constructor assumes live distribution is not needed.
	 * @param scale the maximum scale of the outer random range. Large values will result in more reliable calculations
	 */
	public RandomBias(long scale) {
		this(scale, false);
	}
	
	/**
	 * 
	 * @param scale the maximum scale of the outer random range. Large values will result in more reliable calculations.
	 * @param liveDistrib toggles live distribution on or off
	 */
	public RandomBias(long scale, boolean liveDistrib) {
		this.scale = scale;
		this.liveDistrib = liveDistrib;
		this.updated = liveDistrib;
	}
	
	/**
	 * 
	 * @return the maximum random scale
	 */
	public long getScale() {
		return scale;
	}
	
	/**
	 * 
	 * @return true if distribution is live, false otherwise.
	 */
	public boolean isLive() {
		return liveDistrib;
	}
	
	/**
	 * 
	 * @return true if the outer range of all objects has been updated according to their relative weight, false otherwise.
	 */
	public boolean isUpdated() {
		return this.updated;
	}
	
	/**
	 * 
	 * @param live toggles live distribution on or off
	 */
	public void setLive(boolean live) {
		this.liveDistrib = live;
		this.updated = live;
	}
	
	/**
	 * 
	 * @return an <strong>unmodifiable</strong> representation of the list holding all objects if it holds any, and an empty list otherwise.  
	 */
	public List<? extends RandomObject<? extends Number>> getObjects() {
		return ((this.objects == null)
				? Collections.EMPTY_LIST
				: Collections.unmodifiableList(this.objects));
	}
	
	/**
	 * Adds an object of RandomObject to the list. Also updates all participating objects should live distribution be enabled.
	 * @param obj instance of {@link RandomObject}
	 * @return true if the object was successfully added, false otherwise
	 */
	public boolean addToObjects(RandomObject<? extends Number> obj) {
		boolean changed = false;
		
		if(obj != null) {
			if(this.objects == null) {
				this.objects = new LinkedList<RandomObject>();
			}
			changed = this.objects.add(obj);
		}
		
		if(this.liveDistrib) {
			updateObjectWeight();
		} else {
			this.updated = false;
		}
		return changed;
	}
	
	/**
	 * Removes an object of RandomObject from the list. Also updates all remaining objects should live distribution be enabled.
	 * @param obj instance of {@link RandomObject}
	 * @return true if the object was successfully removed, false otherwise
	 */
	public boolean removeFromObjects(RandomObject<? extends Number> obj) {
		boolean changed = false;
		if((this.objects != null) && (obj != null)) {
			changed  = this.objects.remove(obj);
			obj.dispose();
		}
		
		if(this.liveDistrib) {
			updateObjectWeight();
		} else {
			this.updated = false;
		}
		return changed;
	}
	
	/**
	 * Clears the list of all content with clean deposition.
	 */
	public void removeAllFromObjects() {
		RandomObject<Number> tmp;
		Iterator<? extends RandomObject> itr = this.objects.iterator();
		while(itr.hasNext()) {
			tmp = itr.next();
			this.objects.remove(tmp);
			tmp.dispose();
		}
	}
	
	/**
	 * (Re)distributes the bias based on the scale. This only needs to be called manually if live distribution is disabled.
	 * @return a boolean reflecting on whether anything was changed
	 */
	public boolean updateObjectWeight() {
		boolean changed = false;
		
		if(this.objects.isEmpty())
			return false;
		
		RandomObject tmp;				// holds iterated object
		int size = this.objects.size();	// participating object count
		double sumLast = 0;				// sums up all previous distributions to keep track of the next lower threshold
		Iterator<? extends RandomObject> itr = this.objects.iterator();
		while(itr.hasNext()) {
			tmp = itr.next();
			double range = 0;
			if(tmp.hasWeight()) {
				range = this.scale * tmp.getWeight(); 	// distribution according to fixed weight
			} else {
				range = (this.scale - sumLast) / size; 	// distribution according to relative leftover scale
			}

			tmp.setOuter(sumLast, range + sumLast);
			
			if(!tmp.hasWeight()) {
				tmp.setRelativeWeight(range / this.scale);
			}
			sumLast += range; // keep track of next lower threshold
			size--; // reduces leftover participants for accurate relative leftovers calculation
		}
		return changed;
	}
	
	/**
	 * After successful setup, calling this method returns random values as default double type. Use Number::xyzValue() for clean conversion.
	 * @return an object of {@link Number} containing the randomized value
	 */
	public Number next() {
		if(this.objects.isEmpty()) {
			return -1;
		}
		if(!this.updated) {
			updateObjectWeight();
		}
		
		Number result = null;
		RandomObject<Number> tgt = null;
		double outer = 0;
		
		do {
			outer = Math.random() * this.scale;
			Iterator<? extends RandomObject> itr = this.objects.iterator();
			while(itr.hasNext()) {
				RandomObject tmp = itr.next();
				if(tmp.inRangeOuter(outer)) {
					tgt = tmp;
				}
			}
			if(tgt != null) {
				double l = tgt.getInner().getLow().doubleValue();
				double h = tgt.getInner().getHigh().doubleValue();
				result = (Math.random() * (h - l)) + l;
			}
		} while(tgt == null && result == null);
		
		return result;
	}
	
	public void printObjects() {
		Iterator<? extends RandomObject> itr = this.objects.iterator();
		while(itr.hasNext()) {
			System.out.println(itr.next());
		}
		System.out.println();
	}
	
	@Override
	public String toString() {
		return String.format(
				"New RandomBias object created. Scale %d and %s", 
				this.scale, 
				this.liveDistrib ? "live" : "manual");
	}
}
